package com.dny.asmtop.op;

import com.dny.asmtop.Command;
import com.dny.asmtop.MethodContext;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.Type;
import jdk.internal.org.objectweb.asm.commons.GeneratorAdapter;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import static com.dny.asmtop.ASMMethodUtils.*;
import static java.lang.String.format;
import static jdk.internal.org.objectweb.asm.Type.getType;

/**
 * Created by jlutt on 2018-01-17.
 *
 * @author jlutt
 */
public class CommandCallSuper implements Command {

  private final String methodName;
  private final List<Command> arguments = new ArrayList<>();

  public CommandCallSuper(String methodName, Command... arguments) {
    this.methodName = methodName;
    if ((arguments != null) && (arguments.length > 0)) {
      this.arguments.addAll(Arrays.asList(arguments));
    }
  }

  public CommandCallSuper() {
    this.methodName = null;
  }

  @Override
  public Type type(MethodContext context) {
    List<Class<?>> argumentClasses = new ArrayList<>();

    if ((methodName == null) && (arguments.isEmpty())) {
      //如果参数为空，表示直接在继承方法中调用父类方法
      return context.getMethod().getReturnType();
    }

    List<Type> argumentTypes = new ArrayList<>();
    for (Command argument : arguments) {
      argumentTypes.add(argument.type(context));
      if (argument.type(context).equals(getType(Object[].class))) {
        argumentClasses.add(Object[].class);
      } else {
        argumentClasses.add(getJavaType(context.getClassLoader(), argument.type(context)));
      }
    }
    Type returnType;
    try {
      Class<?> superJavaType = context.getMainClass();
      Method method = superJavaType.getMethod(methodName, argumentClasses.toArray(new Class<?>[]{}));
      Class<?> returnClass = method.getReturnType();
      returnType = getType(returnClass);
    } catch (NoSuchMethodException e) {
      throw new RuntimeException(String.format("No method %s.%s(%s). %s",
          context.getMainClass().getSimpleName(),
          methodName,
          (!argumentClasses.isEmpty() ? argsToString(argumentClasses) : ""),
          exceptionInGeneratedClass(context)
      ));
    }

    return returnType;
  }

  @Override
  public Type generator(MethodContext context) {
    GeneratorAdapter g = context.getGeneratorAdapter();

    g.loadThis();

    if ((methodName == null) && (arguments.isEmpty())) {
      //表示在继承方法中直接调用super对应的方法
      //直接把所有参数推送到执行堆栈，不需要像下面的方式需要循环
      g.loadArgs();

      Type returnType;

      Class<?> superJavaType = context.getMainClass();
      returnType = context.getMethod().getReturnType();
      //调用方法，GeneratorAdapter未提供直接调用父类的方法，这里用MethodVisitor操作
      Type superOwnerType = getType(superJavaType);
      String owner = superOwnerType.getSort() == Type.ARRAY ? superOwnerType.getDescriptor() : superOwnerType.getInternalName();

      g.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, context.getMethod().getName(),
          context.getMethod().getDescriptor(), false);

      return returnType;
    } else {
      //压入参数
      List<Class<?>> argumentClasses = new ArrayList<>();
      List<Type> argumentTypes = new ArrayList<>();
      for (Command argument : arguments) {
        argument.generator(context);
        argumentTypes.add(argument.type(context));
        argumentClasses.add(getJavaType(context.getClassLoader(), argument.type(context)));
      }

      Type returnType;
      try {
        Class<?> superJavaType = context.getMainClass();
        Method method = superJavaType.getMethod(methodName, argumentClasses.toArray(new Class<?>[]{}));
        Class<?> returnClass = method.getReturnType();
        returnType = getType(returnClass);

        //调用方法，GeneratorAdapter未提供直接调用父类的方法，这里用MethodVisitor操作
        Type superOwnerType = getType(superJavaType);
        String owner = superOwnerType.getSort() == Type.ARRAY ? superOwnerType.getDescriptor() : superOwnerType.getInternalName();
        jdk.internal.org.objectweb.asm.commons.Method callMethod = new jdk.internal.org.objectweb.asm.commons.Method(methodName,
            returnType, argumentTypes.toArray(new Type[]{}));
        g.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, callMethod.getName(),
            callMethod.getDescriptor(), false);

      } catch (NoSuchMethodException e) {
        throw new RuntimeException(format("No method %s.%s(%s). %s",
            context.getMainClass().getSimpleName(),
            methodName,
            (!argumentClasses.isEmpty() ? argsToString(argumentClasses) : ""),
            exceptionInGeneratedClass(context)));
      }
      return returnType;
    }
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    CommandCallSuper that = (CommandCallSuper) o;
    return Objects.equals(methodName, that.methodName) &&
        Objects.equals(arguments, that.arguments);
  }

  @Override
  public int hashCode() {
    return Objects.hash(methodName, arguments);
  }
}
